package com.citrix.gs.util.beacon.job;

import java.io.IOException;
import java.io.StringReader;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

import javax.annotation.Nonnull;

import org.apache.xml.utils.SuballocatedByteVector;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.Node;
import org.dom4j.io.SAXReader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.citrix.gs.util.RepositoryDeployer;
import com.citrix.gs.util.beacon.BeaconCI;
import com.citrix.gs.util.beacon.BeaconJobType;
import com.citrix.gs.util.beacon.JobConfig;
import com.citrix.gs.util.beacon.Repository;

public class BeaconJob { //TODO: add edit job schedule time
    
    private static Logger logger = LoggerFactory.getLogger(BeaconJob.class);
            
    protected RepositoryDeployer deployer;
    
    protected BeaconCI testCi;
    protected BeaconCI prodCi;
    protected Repository repo = null;
    
    public String getRepoName() {
        
        if(repo != null) return repo.getName();
        
        if(jobName != null) return jobName.substring(jobName.indexOf("-") +1);
        
        return null;
    }
    
    protected BeaconJobType jobType;
    protected String jobName = null;
    
    protected String tempFile;
    protected Document doc;
    
    public Document getDoc() {
        return doc;
    }

    public BeaconJob setDoc(@Nonnull Document doc) {
        this.doc = doc;
        return this;
    }

    public BeaconJob(){
        
    }
    
    public Repository getRepo() {
        return repo;
    }

    public BeaconJob setRepo(Repository repo) {
        this.repo = repo;
        return this;
    }

    public BeaconJob(RepositoryDeployer deployer){
        this.deployer = deployer;
        this.testCi = deployer.getTestCi();
        this.prodCi = deployer.getProdCi();
        this.repo = deployer.getRepo();
    }
    
    protected Boolean beforeAdd(){
        logger.info("Adding {} job: {}", jobType, jobName);
        
        //String tempName = jobType.name().toLowerCase() + "_template.xml";
        doc = deployer.getTempDoc(tempFile);
        return !(doc == null);
    }
    
    protected Boolean beforeMove() {
        logger.info("Moving {} job: {} from {} to {}", jobType, jobName, testCi.getHostURL(), prodCi.getHostURL());
        
        doc = testCi.getJobDocument(jobName);
        
        return !(doc == null);
    }
    
    protected boolean updateClient(){
        String xml = doc.asXML();
        String patternString = JobConfig.getTestClientPrefix();
        Pattern pattern = Pattern.compile(patternString);
        xml = pattern.matcher(xml).replaceAll(JobConfig.getProdClientPrefix());
        SAXReader reader = new SAXReader();
        try {
            doc = reader.read(new StringReader(xml));
        } catch (DocumentException e) {
            logger.error("Fail to parse xml");
            return false;
        }
        
        return true;
    }
    
    protected boolean updateProjectPath(String test, String prod) {
        
        Node projectPathNode = doc.selectSingleNode("//project/scm[@class='org.jenkinsci.plugins.p4scm.P4SCM']/projectPath");
        if(projectPathNode != null) {
            projectPathNode.setText(projectPathNode.getText().replaceAll(test, prod));
        } else {
            logger.error("No scm node found in config xml");
            return false;
        }
        
        return true;
    }
    
    protected boolean editScmTriggerSpec() {
        return setScmTriggerSpec("H H(22-23) * * *");
    }
    
    protected boolean updateScmTriggerSpec() {
        return setScmTriggerSpec("H H(12-15) * * *");
    }
    
    protected boolean setScmTriggerSpec(String spec) {
        
        String xpath = "//project/triggers/hudson.triggers.SCMTrigger/spec";
        Node specNode = doc.selectSingleNode(xpath);
        
        if(specNode != null) {
            specNode.setText(spec);
        } else {
            logger.error("No spec node: {} found in config xml", xpath);
            return false;
        }
        
        return true;
    }
    
    protected boolean editPerforceSCM(String p4User, String p4Passwd, String p4Port, String p4Client, String projectPath) {
        
        Node scmNode = doc.selectSingleNode("//project/scm[@class='org.jenkinsci.plugins.p4scm.P4SCM']");
        if(scmNode != null) {
            // TODO validate if the provided information is valid
            scmNode.selectSingleNode("p4User").setText(p4User); // TODO: add to config
            scmNode.selectSingleNode("p4Passwd").setText(p4Passwd); // TODO: add to config
            scmNode.selectSingleNode("p4Port").setText(p4Port);
            scmNode.selectSingleNode("p4Client").setText(p4Client);
            scmNode.selectSingleNode("projectPath").setText(projectPath);
        } else {
            logger.error("No scm node found in config xml");
            return false;
        }
        return true;
    }
    
    public boolean editPerforceSCM(Map<String, String> props) {
        
        Node scmNode = doc.selectSingleNode("//project/scm[@class='org.jenkinsci.plugins.p4scm.P4SCM']");
        if(scmNode != null) {
            
            props.keySet().stream().forEach(key -> {
                
                Node node = scmNode.selectSingleNode(key);
                
                if(node == null) {
                    logger.error("{} is not found in perforce scm setting", key); //TODO: how to return false when such error happen
                } else {
                    node.setText(props.get(key));
                }
                
            });
            
        } else {
            logger.error("No scm node found in config xml");
            return false;
        }
        return true;
    }
    
    protected static boolean updateBooleanElement(Node parent, String xpathString, boolean toggle) {
        
        Node node = parent.selectSingleNode(xpathString);
        
        if(node != null) {
            
            node.setText(Boolean.toString(toggle).toLowerCase());
            
        } else {
            logger.error("No node found with xpath: {}", xpathString);
            return false;
        }
        
        return true;
    }
    
    public boolean updateAlwaysForceSync(boolean enable) {
        return updateBooleanElement(doc.getRootElement(), "./scm/alwaysForceSync", enable);
    }
    
    public boolean updateForceSync(boolean enable) {
        return updateBooleanElement(doc.getRootElement(), "./scm/forceSync", enable);
    }
    
    public boolean editAssignedNode() {
        Node assignedNode = doc.selectSingleNode("//project/assignedNode");
        if(assignedNode != null) {
            String node = JobConfig.getAssignedNode(repo.getProduct());
            if(node == null) {
                logger.error("No node assigned for {} ", repo.getProduct());
                return false;
            }
            assignedNode.setText(node);
        } else {
            logger.error("No assignedNode node found in config xml");
            return false;
        }
        
        return true;
    }

    public boolean editAuthToken() {
        Node authTokenNode = doc.selectSingleNode("//project/authToken");
        if(authTokenNode != null) {
            authTokenNode.setText(jobName);
        } else {
            logger.error("No authToken node found in config xml");
            return false;
        }
        return true;
    }

    public boolean editNextJob(String nextJobType) {
        Node nextJobNode = doc.selectSingleNode("//project/publishers/hudson.plugins.parameterizedtrigger.BuildTrigger/configs/hudson.plugins.parameterizedtrigger.BuildTriggerConfig/projects");
        if(nextJobNode != null) {
            nextJobNode.setText(String.format("%s-%s", nextJobType, repo.getName()));
        } else {
            logger.error("No next job node found in config xml");
            return false;
        }
        
        return true;
    }
    
    public boolean editUpstreamJob(String upJobType) {
    	Node upJobNode = doc.selectSingleNode("//project/triggers/jenkins.triggers.ReverseBuildTrigger/upstreamProjects");
    	
    	if(upJobNode != null) {
    		upJobNode.setText(String.format("%s-%s", upJobType, repo.getName()));
    	} else {
    		logger.error("No upstream job node found in config xml");
            return false;
    	}
    	
    	return true;
    }
    
    public boolean editArtifactArchiver(String artifacts) {
    	Node artifactsNode = doc.selectSingleNode("//project/publishers/hudson.tasks.ArtifactArchiver/artifacts");
    	
    	if(artifactsNode != null) {
    		artifactsNode.setText(artifacts);
    	} else {
    		logger.error("No artifacts node found in config xml");
            return false;
    	}
    	
    	return true;
    }
    
    
    public boolean editBuildCommand(String command) {
    	Node commandNode = doc.selectSingleNode("//project/builders/hudson.tasks.BatchFile/command");
    	
    	if(commandNode != null) {
    		commandNode.setText(command);
    	} else {
    		logger.error("No command node found in config xml");
            return false;
    	}
    	
    	return true;
    }
    
    public boolean updateBuildCommand(String test, String prod) {
        Node commandNode = doc.selectSingleNode("//project/builders/hudson.tasks.BatchFile/command");
        
        if(commandNode != null) {
            commandNode.setText(commandNode.getText().replace(test, prod));
        } else {
            logger.error("No command node found in config xml");
            return false;
        }
        
        return true;
    }

    public boolean editNodeStalkerBuildWrapper() {
    	
    	Node jobNode = doc.selectSingleNode("//project/buildWrappers/com.datalex.jenkins.plugins.nodestalker.wrapper.NodeStalkerBuildWrapper/job");
    	
    	if(jobNode != null) {
    		jobNode.setText(BeaconJobType.Src.name() + "-" + repo.getName());
    	} else {
    		logger.error("No NodeStalkerBuildWrapper job node found in config xml");
            return false;
    	}
    	
    	return true;
    }
    
    private void emptyNode(Node node) {
        if(null != node && node.hasContent()) {
           @SuppressWarnings("unchecked")
           List<Node> subNodes = node.selectNodes("./*");
           subNodes.forEach(sub -> {
               sub.detach();
           });
        }
    }
    
    public boolean emptyTriggers(){
        String xpathString = "//project/triggers";
        Node triggersNode = doc.selectSingleNode(xpathString); 
        
        if(triggersNode != null) {
            logger.info("Found triggers node: {}, empty it...", triggersNode.asXML());
            emptyNode(triggersNode);
        } else {
            logger.warn("No triggers found in job config with xpath: {}", xpathString);
        }
        
        return true;
    }
    
    @SuppressWarnings("unchecked")
    public boolean emptyPublishers() {
        String xpathString = "//project/publishers";
        Node pubNode = doc.selectSingleNode(xpathString); 
        
        if(pubNode != null) {
            logger.info("Found publishers node: {}, empty it...", pubNode.asXML());
            if(pubNode.hasContent()) {
                pubNode.selectNodes("./*").forEach(n -> {
                    ((Node)n).detach();
                });
            }
            
        } else {
            logger.warn("No publishers found in job config with xpath: {}", xpathString);
        }
       
        return true;
    }
    
    /*
     * 
     <hudson.tasks.ArtifactArchiver>
      <artifacts>*.xml,*.properties</artifacts>
      <allowEmptyArchive>true</allowEmptyArchive>
      <onlyIfSuccessful>true</onlyIfSuccessful>
      <fingerprint>false</fingerprint>
      <defaultExcludes>true</defaultExcludes>
    </hudson.tasks.ArtifactArchiver>
     */
    public boolean addArtifactArchiver(){
        String xpathString = "//project/publishers";
        Node publishersNode = doc.selectSingleNode(xpathString);
        
        if(publishersNode == null) {
            logger.warn("No publishers found in job config with xpath: {}", xpathString);
            return false;
        }
        
        if(publishersNode.selectSingleNode("./hudson.tasks.ArtifactArchiver") != null) {
            logger.warn("There has been a hudson.tasks.ArtifactArchiver exist");
            return false;
        }
        
        Element publishers = (Element)publishersNode;
        
        Element artifactArchiver = publishers.addElement("hudson.tasks.ArtifactArchiver");
        artifactArchiver.addElement("artifacts").setText("*.xml,*.properties");
        artifactArchiver.addElement("allowEmptyArchive").setText("true");
        artifactArchiver.addElement("onlyIfSuccessful").setText("true");
        artifactArchiver.addElement("fingerprint").setText("false");
        artifactArchiver.addElement("defaultExcludes").setText("true");
        
        return true;
    }
    
    
    /** Example:
     *      <com.michelin.cio.hudson.plugins.copytoslave.CopyToMasterNotifier plugin="copy-to-slave@1.4.3">
              <includes>validchangelog.xml</includes>
              <excludes></excludes>
              <overrideDestinationFolder>true</overrideDestinationFolder>
              <destinationFolder>$JENKINS_HOME/jobs/$JOB_NAME</destinationFolder>
              <runAfterResultFinalised>true</runAfterResultFinalised>
            </com.michelin.cio.hudson.plugins.copytoslave.CopyToMasterNotifier>
     * @return
     */
    public boolean addCopyToMasterNotifier() {
        
        String xpathString = "//project/publishers";
        Node publishersNode = doc.selectSingleNode(xpathString);
        
        if(publishersNode == null) {
            logger.warn("No publishers found in job config with xpath: {}", xpathString);
            return false;
        }
        
        if(publishersNode.selectSingleNode("./com.michelin.cio.hudson.plugins.copytoslave.CopyToMasterNotifier") != null) {
            logger.warn("There has been a com.michelin.cio.hudson.plugins.copytoslave.CopyToMasterNotifier exist");
            return false;
        }
        
        Element publishers = (Element)publishersNode;
        
        Element artifactArchiver = publishers.addElement("com.michelin.cio.hudson.plugins.copytoslave.CopyToMasterNotifier");
        artifactArchiver.addAttribute("plugin", "copy-to-slave@1.4.3");
        artifactArchiver.addElement("includes").setText("validchangelog.xml");
        artifactArchiver.addElement("excludes").setText("");
        artifactArchiver.addElement("overrideDestinationFolder").setText("true");
        artifactArchiver.addElement("destinationFolder").setText("$JENKINS_HOME/jobs/$JOB_NAME");
        artifactArchiver.addElement("runAfterResultFinalised").setText("true");
        
        return true;
    }
    
    
    /** Example:
     *      <hudson.tasks.Mailer plugin="mailer@1.11">
                <recipients>svcacct_gsnkg@citrix.com</recipients>
                <dontNotifyEveryUnstableBuild>false</dontNotifyEveryUnstableBuild>
                <sendToIndividuals>false</sendToIndividuals>
            </hudson.tasks.Mailer>
     * @return
     */
    public boolean addMailer() {
        
        String xpathString = "//project/publishers";
        Node publishersNode = doc.selectSingleNode(xpathString);
        
        if(publishersNode == null) {
            logger.warn("No publishers found in job config with xpath: {}", xpathString);
            return false;
        }
        
        if(publishersNode.selectSingleNode("./hudson.tasks.Mailer") != null) {
            logger.warn("There has been a hudson.tasks.Mailer exist");
            return false;
        }
        
        Element publishers = (Element)publishersNode;
        
        Element mailer = publishers.addElement("hudson.tasks.Mailer");
        mailer.addAttribute("plugin", "mailer@1.11");
        mailer.addElement("recipients").setText("svcacct_gsnkg@citrix.com");
        mailer.addElement("dontNotifyEveryUnstableBuild").setText("false");
        mailer.addElement("sendToIndividuals").setText("false");
        
        return true;
    }
    
    /*
     * Ex.:
     * <hudson.triggers.SCMTrigger>
          <spec>H H(12-15) * * *</spec>
          <ignorePostCommitHooks>false</ignorePostCommitHooks>
       </hudson.triggers.SCMTrigger>
     */
    public boolean addScmTrigger(@Nonnull String spec) {
        
        String xpathString = "//project/triggers";
        Node triggersNode = doc.selectSingleNode(xpathString);
        
        if(triggersNode != null) {
            if(triggersNode.selectSingleNode("./hudson.triggers.SCMTrigger") != null) {
                logger.warn("There has been a ScmTrigger exist");
                return false;
            }
            
            Element triggers = (Element)triggersNode;
            
            Element reverseBuildTrigger = triggers.addElement("hudson.triggers.SCMTrigger");
            
            reverseBuildTrigger.addElement("spec").setText(spec);
            reverseBuildTrigger.addElement("ignorePostCommitHooks").setText("false");
            
        } else {
            logger.warn("No triggers found in job config with xpath: {}", xpathString);
            return false;
        }
        
        return true;
    }
    
    public boolean addScmTriggerForTest() {
        return addScmTrigger("H H(22-23) * * *");
    }
    
    public boolean addScmTriggerForProd() {
        return addScmTrigger("H H(12-15) * * *");
    }
    
    public boolean addReverseBuildTrigger(@Nonnull String preJob) {
        String xpathString = "//project/triggers";
        Node triggerNode = doc.selectSingleNode(xpathString); 
        
        if(triggerNode != null) {
            
            if(triggerNode.selectSingleNode("./jenkins.triggers.ReverseBuildTrigger") != null) {
                logger.warn("There has been a ReverseBuildTrigger exist");
                return false;
            }
            
            Element triggers = (Element)triggerNode;
            
            Element reverseBuildTrigger = triggers.addElement("jenkins.triggers.ReverseBuildTrigger");
            
            reverseBuildTrigger.addElement("spec");
            reverseBuildTrigger.addElement("upstreamProjects").setText(preJob);
            Element threshold = reverseBuildTrigger.addElement("threshold");
            threshold.addElement("name").setText("SUCCESS");
            threshold.addElement("ordinal").setText("0");
            threshold.addElement("color").setText("BLUE");
            threshold.addElement("completeBuild").setText("true");
            
        } else {
            logger.warn("No triggers found in job config with xpath: {}", xpathString);
            return false;
        }
        
        return true;
    }
    
    public boolean removeCustomWorkspace() {
        
        String xpathString = "//project/customWorkspace";
        Node cwNode = doc.selectSingleNode(xpathString);
        
        if(cwNode != null) {
            logger.info("Found customWorkspace node: {}, deleting it...", cwNode.asXML());
            if(cwNode.detach() == null) {
                logger.error("Failed to detach the customWorkspace node");
                return false;
            }
        } else {
            logger.warn("No customWorkspace found in job config with xpath: {}", xpathString);
        }
        
        return true;
    }
    
    private void upsertNodeStalker(Element bwElement) {
        
        Element nodeStalkerBuildWrapper = (Element) bwElement.selectSingleNode("com.datalex.jenkins.plugins.nodestalker.wrapper.NodeStalkerBuildWrapper");
        
        if(nodeStalkerBuildWrapper != null) { // TODO: update it if need
            nodeStalkerBuildWrapper.selectSingleNode("job").setText(BeaconJobType.Src.name() + "-" + getRepoName());
            nodeStalkerBuildWrapper.selectSingleNode("shareWorkspace").setText("true");
            nodeStalkerBuildWrapper.selectSingleNode("firstTimeFlag").setText("true");
            return;
        }
        
        nodeStalkerBuildWrapper = bwElement.addElement("com.datalex.jenkins.plugins.nodestalker.wrapper.NodeStalkerBuildWrapper")
        .addAttribute("plugin", "job-node-stalker@1.0.3");
        
        nodeStalkerBuildWrapper.addElement("job").addText(BeaconJobType.Src.name() + "-" + getRepoName());
        nodeStalkerBuildWrapper.addElement("shareWorkspace").addText("true");
        nodeStalkerBuildWrapper.addElement("firstTimeFlag").addText("true");  
    }
    
    public boolean upsertBuildWrappers() {
        
        String xpathString = "//project/buildWrappers";
        
        Node bwNode = doc.selectSingleNode(xpathString);
        
        if(bwNode == null) {
            logger.info("No buildWrappers node found, add a new one");
            upsertNodeStalker(doc.getRootElement().addElement("buildWrappers"));
        } else {
            if(bwNode.getNodeType() == Node.ELEMENT_NODE) {
               Element bwElement =  (Element)bwNode;
               upsertNodeStalker(bwElement);
            } else {
                return false;
            }
        }
        
        return true;
    }
    
    public String getAssignedNode() {
        
        String xpathString = "//project/assignedNode";
        
        Node anNode = doc.selectSingleNode(xpathString);
        
        if(anNode != null) {
            return anNode.getText();
        }
        
        return null;
    }
    
    public boolean isDisabled() {
        String xpathString = "//project/disabled";
        
        Node disabledNode = doc.selectSingleNode(xpathString);
        
        if(disabledNode != null) {
            return Boolean.parseBoolean(disabledNode.getText());
        }
        
        return false;
    }

    
    
    public boolean createJob() {
        return createJob(testCi);
    }
    
    public boolean moveJob() {
        return createJob(prodCi);
    }
    
    private boolean createJob(BeaconCI ci) {
        logger.debug("job xml: {} ", doc.asXML());
        
        if(!ci.addJob(jobName, doc.asXML(), false)) {
           logger.error("Fail to add job {} on ci {} ", jobName, ci.getHostURL());
           return false; 
        } else {
           logger.info("Job {} has been added to CI: {}", jobName, ci.getHostURL());
        }
        
        return true;
    }
    
    public void delete() {
        delete(testCi);
    }
    
    public void delete(BeaconCI ci) {
        try {
            if(ci.getJob(jobName) != null) {
                logger.info("Deleting job: {}", jobName);
                ci.deleteJob(jobName, false);
                logger.info("Deleted");
            }
        } catch (IOException e) {
            logger.error("Fail to delete job: {}", jobName, e);
        }
    }

    public BeaconJobType getJobType() {
        return jobType;
    }
    

    public BeaconJob setJobType(BeaconJobType jobType) {
        this.jobType = jobType;
        return this;
    }
    

    public String getJobName() {
        return jobName;
    }
    

    public BeaconJob setJobName(String jobName) {
        this.jobName = jobName;
        return this;
    }
 
}
